﻿/******************************************************************************
 *      Class:      WeatherDataPointCollection
 *      Authors:    Michael Slezak, Samuel Harrington
 *      Implements: INotifyPropertyChanged
 *      
 * ----------------------------------------------------------------------------
 * 
 *      Description:
 *          This is the heart of the application, coordinating the
 *          WeatherDataReader and the gui by keeping track of what range the
 *          gui is showing, providing the points in that range, aggregate
 *          functions over the points, and methods and properties to change
 *          the displayed range.
 *          
 ******************************************************************************/

////////////////////////////////////////////////////////////////////////////////
//   INCLUDES
////////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;

namespace WeatherStationProgram
{
    class WeatherData : INotifyPropertyChanged
    {        
        #region Class Members...

        public event PropertyChangedEventHandler PropertyChanged;

        // The reader for the data
        private WeatherDataReader reader = new WeatherDataReader();
        
        // The date/time range info
        private DateTime _start;
        private DateTime _end;        
        private RangeType _rangeSize = RangeType.Day;
        private WeatherDataPoint.Members _dataDisplayed;

        // Status info
        private bool _hasDirectory = false;
        private bool _canNext = true;
        private bool _canPrev = true;
        private string _dateMessage = "";

        // Drop-down contents
        private List<int> _yearsAvailable;
        private List<int> _monthsAvailable;
        
        // Aggregate values
        private WeatherDataPoint _sum;
        private WeatherDataPoint _average;
        private WeatherDataPoint _max;
        private WeatherDataPoint _min;

        private DateTime _minTempDate;
        private DateTime _maxTempDate;
        private DateTime _maxWindGustDate;

        #endregion

        /// <summary>
        /// Basic setup
        /// </summary>
        public WeatherData()
        {
            YearsAvailable = new List<int>();
            MonthsAvailable = new List<int>();
            DaysAvailable = new List<int>();

            Points = new WeatherDataPointCollection();
        }        
        
        #region Simple properties

        /// <summary>
        /// The data to be shown
        /// </summary> 
        public WeatherDataPoint.Members DataDisplayed
        {
            get
            {
                return _dataDisplayed;
            }
            set
            {
                _dataDisplayed = value;
                UpdateProperty("DataDisplayed");
            }
        }

        #region Status info

        public bool HasDirectory
        {
            get
            {
                return _hasDirectory;
            }
            set
            {
                _hasDirectory = value;
                UpdateProperty("HasDirectory");
            }
        }

        public bool CanNext
        {
            get
            {
                return _canNext;
            }
            set
            {
                _canNext = value;
                UpdateProperty("CanNext");
            }
        }
        public bool CanPrev
        {
            get { return _canPrev; }
            set
            {
                _canPrev = value;
                UpdateProperty("CanPrev");
            }
        }

        public string DateMessage
        {
            get
            {
                return _dateMessage;
            }            
            set
            {
                _dateMessage = value;
                UpdateProperty("DateMessage");
            }
        }

        #endregion

        #region Aggregate values

        public WeatherDataPoint Sum
        {
            get
            {
                return _sum;
            }
            set
            {
                _sum = value;
                UpdateProperty("Sum");
            }
        }
        public WeatherDataPoint Average
        {
            get
            {
                return _average;
            }
            set
            {
                _average = value;
                UpdateProperty("Average");
            }
        }
        public WeatherDataPoint Max
        {
            get
            {
                return _max;
            }
            set
            {
                _max = value;
                UpdateProperty("Max");
            }
        }
        public WeatherDataPoint Min
        {
            get
            {
                return _min;
            }
            set
            {
                _min = value;
                UpdateProperty("Min");
            }
        }

        public DateTime MinTempDate
        {
            get
            {
                return _minTempDate;
            }
            set
            {
                _minTempDate = value;
                UpdateProperty("MinTempDate");
            }
        }
        public DateTime MaxTempDate
        {
            get
            {
                return _maxTempDate;
            }
            set
            {
                _maxTempDate = value;
                UpdateProperty("MaxTempDate");
            }
        }
        public DateTime MaxWindGustDate
        {
            get
            {
                return _maxWindGustDate;
            }
            set
            {
                _maxWindGustDate = value;
                UpdateProperty("MaxWindGustDate");
            }
        }

        #endregion

        #region Drop-down contents

        public List<int> YearsAvailable
        {
            get
            {
                return _yearsAvailable;
            }
            set
            {
                _yearsAvailable = value;
                UpdateProperty("YearsAvailable");
            }
        }
        public List<int> MonthsAvailable
        {
            get
            {
                return _monthsAvailable;
            }
            set
            {
                _monthsAvailable = value;
                UpdateProperty("MonthsAvailable");
            }
        }
        public List<int> DaysAvailable { get; set; }

        #endregion

        #region The points graphed

        public WeatherDataPointCollection Points { get; set; }
        public List<WeatherDataPoint> AllPoints { get; set; }

        #endregion

        #endregion

        #region Properties

        /// <summary>
        /// The range size
        /// On update it changes the Start and End to match
        /// </summary>
        public RangeType RangeSize
        {
            get
            {
                return _rangeSize;
            }
            set
            {
                _rangeSize = value;

                switch (RangeSize)
                {
                    case RangeType.Year:
                        Month = 1;
                        Day = 1;
                        break;
                    case RangeType.Month:
                        Month = MonthsAvailable.Contains(Month) ? Month : MonthsAvailable.First();
                        Day = 1;
                        break;
                    case RangeType.Week:
                        if (Start < reader.all_points.First().date)
                            Start = reader.all_points.First().date;
                        Start = Start.Subtract(TimeSpan.FromDays((double)Start.DayOfWeek));
                        break;
                    case RangeType.Day:
                        Month = MonthsAvailable.Contains(Month) ? Month : MonthsAvailable.First();
                        Day = DaysAvailable.Contains(Day) ? Day : DaysAvailable.First();
                        break;
                }

                CanNext = CanDoNext();
                CanPrev = CanDoPrev();
                UpdateProperty("RangeSize");
            }
        }

        /// <summary>
        /// The current (Start) year
        /// On update it changes Start and End
        /// Valid to use in all except Week and Custom
        /// </summary>
        public int Year
        {
            get { return Start.Year; }
            set
            {
                Start = Start.AddYears(value - Start.Year);
            }
        }

        /// <summary>
        /// The current (Start) month
        /// On update it changes Start and End
        /// Valid to use in Month, Day
        /// </summary>
        public int Month
        {
            get { return Start.Month; }
            set
            {
                Start = Start.AddMonths(value - Start.Month);
            }
        }

        /// <summary>
        /// The current (Start) day
        /// On update it changes Start and End
        /// Valid to use in Day
        /// </summary>
        public int Day
        {
            get { return Start.Day; }
            set
            {
                Start = Start.AddDays(value - Start.Day);
            }
        }

        /// <summary>
        /// The start date/time
        /// On update it also updates the End/messages/points/etc
        /// </summary>
        public DateTime Start
        {
            get { return _start; }
            set
            {
                bool ych = _start.Year != value.Year;
                bool mch = _start.Month != value.Month;
                bool dch = _start.Day != value.Day;

                _start = value;

                if (ych)
                    MonthsAvailable = new List<int>(reader.point_dates[Start.Year].Keys);
                if (ych || mch)
                {
                    if (reader.point_dates[Start.Year].ContainsKey(Start.Month))
                        DaysAvailable = new List<int>(reader.point_dates[Start.Year][Start.Month]);
                    else
                        DaysAvailable = new List<int>();
                }

                switch (RangeSize)
                {
                    case RangeType.Year: End = Start.AddYears(1); break;
                    case RangeType.Month: End = Start.AddMonths(1); break;
                    case RangeType.Week: End = Start.AddDays(7); break;
                    case RangeType.Day: End = Start.AddDays(1); break;
                    case RangeType.Custom: onUpdateRange(); break;
                }
                
                UpdateProperty("Start");
                if (ych) UpdateProperty("Year");
                if (mch) UpdateProperty("Month");
                if (dch) UpdateProperty("Day");
            }
        }

        /// <summary>
        /// The end date/time
        /// On update it also updates the messages/points/etc
        /// </summary>
        public DateTime End
        {
            get { return _end; }
            set
            {
                _end = value;
				onUpdateRange();
                UpdateProperty("End");
            }
        }

        #endregion               
        
        /// <summary>
        /// Load the data from the specified data
        /// </summary>
        /// <param name="dir">the directory to load from</param>
        public void SetDirectory(string dir)
        {
            reader.SetDirectory(dir);
            
            HasDirectory = reader.all_points.Count > 0;

            YearsAvailable.Clear();

            if (!HasDirectory)
                return;

            foreach (int i in reader.point_dates.Keys)
                YearsAvailable.Add(i);
            
            Year = YearsAvailable.Last();
       
            CanNext = CanDoNext();
            CanPrev = CanDoPrev();
        }   

        #region Private functions
        
        /// <summary>
        /// Inform wpf of a property update
        /// </summary>
        /// <param name="property"></param>
        private void UpdateProperty(string property)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }

        /// <summary>
        /// Update the message to be displayed
        /// </summary>
        private void updateMessage()
        {
            switch (RangeSize)
            {
                case RangeType.Day:
                    DateMessage = Start.ToString("ddd d MMM yyyy");
                    break;
                case RangeType.Week:
                    DateMessage = Start.ToString("ddd d MMM yyyy") + " - " + End.Subtract(TimeSpan.FromSeconds(1)).ToString("ddd d MMM yyyy");
                    break;
                case RangeType.Month:
                    DateMessage = Start.ToString("MMM yyyy");
                    break;
                case RangeType.Year:
                    DateMessage = Start.ToString("yyyy");
                    break;
            }
        }

        /// <summary>
        /// Update the points shown
        /// </summary>
        private void updatePoints()
        {
            Points.Clear();
            Points.SendUpdate();

            IEnumerable<WeatherDataPoint> pts_in_range = reader.all_points.SkipWhile(p => p.date < Start).TakeWhile(p => p.date < End);
            double pct = 500d / pts_in_range.Count();
            Random random = new Random();
            foreach (WeatherDataPoint pt in pts_in_range)
            {
                if (random.NextDouble() <= pct)  // extract random points from the points in range
                    Points.Add(pt);
            }
            Points.SendUpdate();

            AllPoints = new List<WeatherDataPoint>(pts_in_range);
        }

        /// <summary>
        /// Update the min/max/avg/sums
        /// </summary>
        private void updateAggregates()
        {
            WeatherDataPoint sum = new WeatherDataPoint(), max = new WeatherDataPoint(), min = new WeatherDataPoint();
            Vector windsum = new Vector(0, 0);  // Average of the directions converted to unit vectors
            max.barometer = max.heatIndex = max.humidity = max.rainfall
                = max.temperature = max.uvIndex = max.windChill
                = max.windGust = max.windSpeed = float.MinValue;
            min.barometer = min.heatIndex = min.humidity = min.rainfall
                = min.temperature = min.uvIndex = min.windChill
                = min.windGust = min.windSpeed = float.MaxValue;
            foreach (WeatherDataPoint p in AllPoints)
            {
                sum.rainfall += p.rainfall;
                sum.temperature += p.temperature;
                sum.windSpeed += p.windSpeed;
                windsum += new Vector(Math.Cos((int)p.windDirection * Math.PI / 8), Math.Sin((int)p.windDirection * Math.PI / 8));

                if (p.temperature > max.temperature)
                    MaxTempDate = p.date;
                if (p.temperature < min.temperature)
                    MinTempDate = p.date;
                if (p.windGust > max.windGust)
                    MaxWindGustDate = p.date;

                max.temperature = Math.Max(max.temperature, p.temperature);
                max.windGust = Math.Max(max.windGust, p.windGust);

                min.temperature = Math.Min(min.temperature, p.temperature);
                min.windGust = Math.Min(min.windGust, p.windGust);
            }

            WeatherDataPoint avg = new WeatherDataPoint();
            avg.temperature = sum.temperature / AllPoints.Count;
            avg.windSpeed = sum.windSpeed / AllPoints.Count;
            avg.windDirection = (WeatherDataPoint.WindDirection)(Math.Atan2(windsum.Y / AllPoints.Count, windsum.X / AllPoints.Count) * 8 / Math.PI);

            Sum = sum;
            Average = avg;
            Max = max;
            Min = min;
        }

        /// <summary>
        /// The update what is shown based on Start and End
        /// </summary>
        private void onUpdateRange()
        {
            DateTime start_time = DateTime.Now;

            updateMessage();
            updatePoints();
            updateAggregates();

            Console.WriteLine("onUpdateRange {1} to {2} took {0}", DateTime.Now - start_time, Start, End);
        }

        /// <summary>
        /// Go to the next range
        /// </summary>
        public void Next()
        {
            switch (RangeSize)
            {
                case RangeType.Day:
                    Day = Day + 1;
                    break;

                case RangeType.Week:
                    Start = Start.AddDays(7);
                    break;

                case RangeType.Month:
                    Month = Month + 1;
                    break;

                case RangeType.Year:
                    Year = Year + 1;
                    break;
            }

            CanNext = CanDoNext();
            CanPrev = CanDoPrev();
        }

        /// <summary>
        /// Go to the previous range
        /// </summary>
        public void Prev()
        {
            switch (RangeSize)
            {
                case RangeType.Day:
                    Day = Day - 1;
                    break;

                case RangeType.Week:
                    Start = Start.AddDays(-7);
                    break;

                case RangeType.Month:
                    Month = Month - 1;
                    break;

                case RangeType.Year:
                    Year = Year - 1;
                    break;
            }

            CanPrev = CanDoPrev();
            CanNext = CanDoNext();
        }

        /// <summary>
        /// Can we go forward?
        /// </summary>
        /// <returns></returns>
        private bool CanDoNext()
        {
            return reader.all_points.Last().date >= End;
        }

        /// <summary>
        /// Can we go backward?
        /// </summary>
        /// <returns></returns>
        private bool CanDoPrev()
        {
            return reader.all_points.First().date < Start;
        }

        #endregion
    }

    public enum RangeType
    {
        Day,
        Week,
        Month,
        Year,
        Custom
    }    
}
